只是個幫助忙碌的專業人士和父母找回時間、平衡生活的斜槓老爸。 我探索人生的大小賽局,分享優化人生的實用觀點(關於人類、科技和未來)。
在我的個人網站上獲取最新的觀點:https://klkuo.guru

在後端學習的過程中,不時需要復刻我們生活中常使用的工具,來刻意練習。雖然 MongoDB 是 NoSQL 的資料庫,但現實中非常多的概念是互相關聯的。以記帳為例,每筆記錄都對應到一個類別,而每個類別下都包含了數筆不同的紀錄。那在建置資料庫及資料操作時,究竟如何設計,能大幅度提高資料庫的效能,而不重複存放非常多相同名稱的類別資料呢?
為了解決此問題,在作業中我嘗試使用 MongoDB 搭配 Mongoose 來攻略關聯式資料庫的建置與操作。
本篇筆記將解決以下問題:
.populate() 方法如何協助我們運用關聯資料?誰適合閱讀:
參考資料:
 
本篇筆記將依照 產品工匠日常:打造全端產品的宏觀程序 中,資料庫架設及功能實作的架構及順序來記錄,我如何設計 Data Model(Schema)及以 .populate() 關聯資料,僅摘錄部分程式碼,細節請參考 GitHub repo:
關聯部份設定路徑,用以存放對應資料的 ObjectId:
models/category.js
const categorySchema = new Schema({
  title: {
    type: String,
    trim: true,
    required: true
  },
  icon: {
    type: String,
    trim: true,
    required: true
  },
  records: [{
    type: Schema.Types.ObjectId,
    ref: 'Record'
  }]
})
module.exports = mongoose.model('Category', categorySchema)
models/record.js
const recordSchema = new Schema({
  name: {
    type: String,
    trim: true,
    required: true
  },
  category: {
    type: Schema.Types.ObjectId,
    ref: 'Category'
  },
  date: {
    type: String,
    required: true
  },
  amount: {
    type: Number,
    min: [1, 'at least one dollar'],
    required: true
  }
})
module.exports = mongoose.model('Record', recordSchema)
先新增類別,再新增種子記錄,以撈取對應的類別。比較麻煩的部分是我想兩個 collection 都更新對應資料,所以有兩層的資料操作:
$ node models/seeds/categorySeeder.js
const categories = [
  ['家居物業', 'fa-home'],
  ['交通出行', 'fa-shuttle-van'],
  ['休閒娛樂', 'fa-grin-beam'],
  ['餐飲食品', 'fa-utensils'],
  ['其他', 'fa-pen']
].map(category => ({
  title: category[0],
  icon: `<i class="fas ${category[1]}"></i>`
}))
// Generate category seed
db.once('open', () => {
  Category.create(categories)
    .then(() => {
      db.close()
    })
  console.log('categorySeeder.js done ^_^')
})
$ node models/seeds/recordSeeder.js
db.once('open', () => {
  createRecords()
  console.log('recordSeeder.js done ^_^')
})
function createRecords() {
  Category.find()
    .then(categories => {
      const categoriesId = []
      categories.forEach(category => {
        categoriesId.push(category._id)
      })
      return categoriesId // 含有所有類別 _id 的 array
    })
    .then(id => {
      for (let i = 0; i < 5; i++) {
        Record.create({ // 新增紀錄
          name: `name-${i}`,
          category: id[i],
          date: `2020-09-0${i + 1}`,
          amount: (i + 1) * 100
        })
          .then(record => { // 將對應紀錄存入類別的 collection 中
            Category.findById(id[i])
              .then(category => {
                category.records.push(record._id)
                category.save()
              })
          })
      }
    })
    .catch(error => console.error(error))
}
最複雜的部分屬 CRUD 的操作,因為想同步 records 和 categories 的 collections。
P.s. 後來在 MDN 上才發現,其實也不一定要同步,可以統一更新在其中一個 collection 在使用 .populate() 關聯即可。
// routes/modules/records.js
router.post('/new', (req, res) => {
  const record = req.body // 整筆紀錄存放在 object 中
  Category.findOne({ title: record.category })
    .then(category => {
      record.category = category._id // 找到對應的 category._id
      Record.create(record) // 新增紀錄
        .then(record => {
          category.records.push(record._id) // 更新 categories collection 中對應的類別
          category.save()
        })
        .then(() => res.redirect('/'))
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
})
// routes/modules/home.js
router.get('/', (req, res) => {
  Category.find()
    .lean()
    .sort({ _id: 'asc' })
    .then(checkedCategories => {
      Record.find()
        .populate('category') // 將所有紀錄的類別關聯 categories collection
        .lean()
        .sort({ _id: 'asc' })
        .then(records => {
          let totalAmount = 0
          records.forEach(record => totalAmount += record.amount)
          res.render('index', { records, totalAmount, checkedCategories })
        })
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
})
// routes/modules/records.js
router.put('/:id', (req, res) => {
  const { id } = req.params
  const update = req.body
  
  // remove this record from old category
  Record.findById(id)
    .then(record => {
      Category.findById(record.category)
        .then(category => {
          category.records = category.records.filter(record => record.toString() !== id)
          category.save()
        })
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
  // assign category id in update object
  Category.findOne({ title: update.category })
    .then(category => {
      update.category = category._id
      // update record
      Record.findByIdAndUpdate(id, update, { new: true })
        .then(record => {
          category.records.push(record._id)
          category.save()
        })
        .then(() => res.redirect(`/`))
        .catch(error => console.error(error))
    })
    .catch(error => console.error(error))
})
// routes/modules/records.js
router.delete('/:id', (req, res) => {
  const { id } = req.params
  Record.findById(id)
    .then(record => {
      Category.findById(record.category)
        // remove record from collection of category
        .then(category => {
          category.records = category.records.filter(record => record.toString() !== id)
          category.save()
        })
        .catch(error => console.error(error))
      // delete this record
      record.remove()
    })
    .then(() => res.redirect('/'))
    .catch(error => console.error(error))
})
 

關於本系列更多內容及導讀,請閱讀作者於 Medium 個人專欄 【無限賽局玩家 Infinite Gamer | Publication – 】 上的文章 《用 JavaScript 打造全端產品的入門學習筆記》系列指南。